/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.explorer.data;

import com.vaadin.data.Container;
import com.vaadin.data.Item;
import com.vaadin.data.Property;
import org.activiti.engine.ActivitiException;

import java.util.*;


/**
 * Inspired on the LazyQueryContainer add-on for Vaadin
 * (https://github.com/tlaukkan/vaadin-lazyquerycontainer)
 *
 * @author Joram Barrez
 */
public class LazyLoadingContainer implements Container.Indexed, Container.Sortable {

    private static final long serialVersionUID = 1L;

    protected LazyLoadingQuery lazyLoadingQuery;
    protected int batchSize;
    protected int size = -1;

    protected List<Object> containerPropertyIds = new ArrayList<Object>();
    protected Map<Object, Class<?>> containerPropertyTypes = new HashMap<Object, Class<?>>();
    protected Map<Object, Object> containerPropertyDefaultValues = new HashMap<Object, Object>();

    protected Map<Integer, Item> itemCache = new HashMap<Integer, Item>();

    public LazyLoadingContainer(LazyLoadingQuery lazyLoadingQuery, int batchSize) {
        this.lazyLoadingQuery = lazyLoadingQuery;
        this.batchSize = batchSize;
        lazyLoadingQuery.setLazyLoadingContainer(this);
    }

    public LazyLoadingContainer(LazyLoadingQuery lazyLoadingQuery) {
        this(lazyLoadingQuery, 30);
    }

    public boolean addContainerProperty(Object propertyId, Class<?> type, Object defaultValue) throws UnsupportedOperationException {
        containerPropertyIds.add(propertyId);
        containerPropertyTypes.put(propertyId, type);
        containerPropertyDefaultValues.put(propertyId, defaultValue);
        return true;
    }

    public boolean removeContainerProperty(Object propertyId) throws UnsupportedOperationException {
        containerPropertyIds.remove(propertyId);
        containerPropertyTypes.remove(propertyId);
        containerPropertyDefaultValues.remove(propertyId);
        return true;
    }

    public Collection<?> getContainerPropertyIds() {
        return containerPropertyIds;
    }

    public Class<?> getType(Object propertyId) {
        return containerPropertyTypes.get(propertyId);
    }

    public Object getDefaultValue(Object propertyId) {
        return containerPropertyDefaultValues.get(propertyId);
    }

    public Property getContainerProperty(Object itemId, Object propertyId) {
        return getItem(itemId).getItemProperty(propertyId);
    }

    public boolean containsId(Object itemId) {
        Integer index = (Integer) itemId;
        return index >= 0 && index < size();
    }

    public Item getItem(Object itemId) {
        if (itemId != null) {
            if (!itemCache.containsKey(itemId)) {
                Integer index = (Integer) itemId;
                int start = index - (index % batchSize);

                List<Item> batch = lazyLoadingQuery.loadItems(start, batchSize);
                for (Item batchItem : batch) {
                    itemCache.put(start, batchItem);
                    start++;
                }
            }
            return itemCache.get(itemId);
        }

        return null;
    }

    public int size() {
        if (size == -1) {
            size = lazyLoadingQuery.size();
        }
        return size;
    }

    public Collection<?> getItemIds() {
        // We don't need an actual list of elements,
        // since we map the ids in natural integer order (0,1,2,3, etc)
        // so we can just override the get() and save some memory
        return new AbstractList<Integer>() {
            public int size() {
                return size();
            }

            public Integer get(int index) {
                return index;
            }
        };
    }

    public Object firstItemId() {
        return 0;
    }

    public Object lastItemId() {
        return size() - 1;
    }

    public boolean isFirstId(Object itemId) {
        return ((Integer) itemId).equals(0);
    }

    public boolean isLastId(Object itemId) {
        return ((Integer) itemId).equals(size() - 1);
    }

    public Object nextItemId(Object itemId) {
        Integer index = (Integer) itemId;
        return index++;
    }

    public Object prevItemId(Object itemId) {
        Integer index = (Integer) itemId;
        return index--;
    }

    public int indexOfId(Object itemId) {
        return (Integer) itemId;
    }

    public Object getIdByIndex(int index) {
        return new Integer(index);
    }

    public Collection<?> getSortableContainerPropertyIds() {
        return containerPropertyIds;
    }

    public void sort(Object[] propertyIds, boolean[] ascending) {
        removeAllItems();
        lazyLoadingQuery.setSorting(propertyIds, ascending);
    }

    public boolean removeAllItems() throws UnsupportedOperationException {
        itemCache.clear();
        size = -1;
        return true;
    }

    // binary search using the real id of the object, and map it to an index in the container

    // id = real id (eg task id)
    public int getIndexForObjectId(String id) {
        Item searched = lazyLoadingQuery.loadSingleResult((String) id);
        if (searched == null) {
            return -1;
        }
        return getIndexForObjectId(searched, 0, size() - 1);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    public int getIndexForObjectId(Item searched, int low, int high) {
        if (high < low) {
            return -1; // not found
        }

        int middle = low + (high - low) / 2;
        Item result = null;

        if (itemCache.containsKey(middle)) {
            result = itemCache.get(middle);
        } else {
            result = lazyLoadingQuery.loadItems(middle, 1).get(0);
            itemCache.put(middle, result);
        }

        if (!(searched instanceof Comparable)
                || !(result instanceof Comparable)) {
            throw new ActivitiException("Cannot use the getIndexForObjectId method for non-Comparables");
        }

        int comparison = ((Comparable) searched).compareTo((Comparable) result);
        if (comparison < 0) {
            return getIndexForObjectId(searched, low, middle - 1);
        } else if (comparison > 0) {
            return getIndexForObjectId(searched, middle + 1, high);
        } else {
            return middle;
        }
    }

    // Unsupported Operations ----------------------------------------------------------------------

    public Object addItem() throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    public Object addItemAfter(Object previousItemId) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    public Item addItemAfter(Object previousItemId, Object newItemId) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    public Object addItemAt(int index) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    public Item addItemAt(int index, Object newItemId) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    public boolean removeItem(Object itemId) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    public Item addItem(Object itemId) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

}
